package com.wang.tool.mybatis;

import com.alibaba.fastjson.JSON;
import com.wang.tool.common.ErrorInfo;
import com.wang.tool.common.SimpleException;
import com.wang.tool.monitor.InvokeTemplate;
import com.wang.tool.monitor.LinkLogUtil;
import com.wang.tool.monitor.core.MonitorInfo;
import com.wang.tool.monitor.template.AbsMonitorInvoker;
import com.wang.tool.trace.TraceContext;
import com.wang.tool.trace.TraceTemplate;
import com.wang.tool.util.CommonUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.ArrayUtil;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.Array;
import java.sql.SQLException;
import java.util.*;
import java.util.function.Function;

/**
 * 拦截所有 select(不包含流式查询) update insert delete
 * 摘要记录
 * 自配置实现部分场景模拟mybatis格式输出sql可用于常见工具解析mybatis日志sql
 * 建议此插件放在插件列表最前面
 *
 * @author shaopeng
 */
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class MybatisMonitorInterceptor implements Interceptor {

    /**
     * 配置
     */
    protected final Map<String, String> properties = new HashMap<>();


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
//        预期判断,理论上应该都满足,第一个参数是MappedStatement,第二个参数是sql入参
        if (args.length < 2 || !(args[0] instanceof MappedStatement)) {
            return invocation.proceed();
        }
        MappedStatement statement = (MappedStatement) args[0];
        String biz;
        String action;
        try {
//            应该不会有重复的
            String id = statement.getId();
            String[] split = id.split("\\.");
            biz = split[split.length - 2];
            action = split[split.length - 1];
        } catch (Throwable e) {
            biz = statement.getSqlCommandType().toString();
            action = statement.getId();
        }
        String finalClassName = biz;
        String finalMethod = action;
        String finalBiz = biz;
        String finalAction = action;
        return TraceTemplate.invokeWithNext(new Function<TraceContext, Object>() {
            @Override
            public Object apply(TraceContext context) {
                return InvokeTemplate.monitor("mybatis", finalBiz, finalAction, new AbsMonitorInvoker<Object>(args[1]) {
                    @Override
                    public void weaveOnBefore(MonitorInfo<Object> monitorInfo) throws Throwable {
                        if (context != null) {
                            monitorInfo.addDigest(MonitorInfo.REQ_ID, context.buildReqId());
                            monitorInfo.addDigest(MonitorInfo.TIME, MonitorInfo.formatTimeWithZone(monitorInfo.getBeginTime()));
                        }
                    }

                    @Override
                    public Object internalCall() throws Throwable {
                        return invocation.proceed();
                    }

                    @Override
                    public Object handleError(Throwable e, ErrorInfo error) {
                        throw new SimpleException(e, SimpleException.DAO_ERROR, SimpleException.DAO_ERROR + "." + finalClassName + "." + finalMethod);
                    }

                    @Override
                    protected void internalAfter(MonitorInfo<Object> monitorInfo) throws Throwable {
                        monitorInfo.addDigest("id", statement.getId());
                        Integer row = null;
//                失败恒输出
                        if (monitorInfo.isFail()) {
                            monitorInfo.addDigest("row", row);

                            monitorInfo.addDetailParam(args[1]);

                            String mybatisSql = buildSql(monitorInfo, statement, args, row);
                            monitorInfo.addDetail("sql", mybatisSql);
                            return;
                        }

//                特殊场景输出sql
                        boolean sql = false;
                        Object result = monitorInfo.getResult();
                        if (statement.getSqlCommandType() == SqlCommandType.SELECT) {
                            if (result instanceof Collection) {
                                row = ((Collection<?>) result).size();
                            } else if (result != null) {
                                row = 1;
                            }
//                    缓存sql
                            boolean isCachedQuery = args.length == 6 && args[4] instanceof CacheKey && args[5] instanceof BoundSql;
                            if (isCachedQuery) {
                                LinkLogUtil.newTagLog("SqlSelectHitCache")
                                        .v1(String.valueOf(row))
                                        .log();
                            }
                            sql = isSqlOnSelect(statement.getId(), result, row);
                        } else if (statement.getSqlCommandType() == SqlCommandType.UPDATE) {
                            if (result instanceof Number) {
                                row = ((Number) result).intValue();
                            }
                            sql = isSqlOnUpdate(statement.getId(), result, row);
                        } else if (statement.getSqlCommandType() == SqlCommandType.INSERT) {
                            if (result instanceof Number) {
                                row = ((Number) result).intValue();
                            }
                            sql = isSqlOnInsert(statement.getId(), result, row);
                        } else if (statement.getSqlCommandType() == SqlCommandType.DELETE) {
                            if (result instanceof Number) {
                                row = ((Number) result).intValue();
                            }
                            sql = isSqlOnDelete(statement.getId(), result, row);
                        }
                        monitorInfo.addDigest("row", row);

                        //慢 恒sql 自定义
                        if (sql || isAllSql() || monitorInfo.isSlow()) {
                            monitorInfo.addDetailParam(args[1]);

                            String mybatisSql = buildSql(monitorInfo, statement, args, row);
                            monitorInfo.addDetail("sql", mybatisSql);
                        }
                    }

                });
            }
        });
    }

    /**
     * 构建sql输出
     *
     * @param statement
     * @param args
     * @param row
     * @return
     */
    protected String buildSql(MonitorInfo<?> monitorInfo, MappedStatement statement, Object[] args, Integer row) {
        // 获取 BoundSql 对象，包含 SQL 语句和参数映射信息
        BoundSql boundSql;
        if (args.length == 6) {
            // 带缓存的 query 方法
            boundSql = (BoundSql) args[5];
        } else {
            // 其他 update 或 query 方法
            boundSql = statement.getBoundSql(args[1]);
        }
//        count 特殊处理
        Number count = null;
        Object result = monitorInfo.getResult();
        // 从 BoundSql 实例中获取 SQL 语句 sql简单去除换行
        String sql = removeBreakingWhitespace(boundSql.getSql());
        if (row != null && row == 1 && result instanceof List && ((List<?>) result).size() == 1 && ((List<?>) result).get(0) instanceof Number) {
//          兼容pageHelper
            if (sql.startsWith("select count(")
                    || sql.startsWith("select COUNT(")
                    || sql.startsWith("SELECT count(")
                    || sql.startsWith("SELECT COUNT(")
                    || statement.getId().endsWith("_COUNT")
            ) {
                count = (Number) ((List<?>) result).get(0);
            }
        }

        // 获取参数值
        String parameterValues = getParameterValues(boundSql, args[1]);

        String countStr = count == null ? "" : ("\n<==        Row: " + count);
        String rowStr = row == null ? "" : statement.getSqlCommandType() == SqlCommandType.SELECT ? ("\n<==      Total: " + row) : ("\n<==    Updates: " + row);
        return "\n==>  Preparing: " + sql + "\n" + "==> Parameters: " + parameterValues + countStr + rowStr + "\n";
    }

    /**
     * 是否恒输出sql
     * 可重写
     *
     * @return
     */
    protected boolean isAllSql() {
        return "true".equals(properties.get("allSql"));
    }


    /**
     * 查询场景是否输出日志
     * 可重写
     *
     * @param id     org.apache.ibatis.mapping.MappedStatement#getId() test.dao.mapper.HouseMapper.select 非空
     * @param result
     * @param row    受影响行数
     * @return
     */
    protected boolean isSqlOnSelect(String id, Object result, Integer row) {
        if (row == null) {
            return false;
        }
        if (row <= getMonitorSize()) {
            return false;
        }
        LinkLogUtil.newTagLog("SqlSelectTooMany")
                .v1(String.valueOf(row))
                .log();
        return true;
    }


    /**
     * 更新场景是否输出日志
     * 可重写
     *
     * @param id     org.apache.ibatis.mapping.MappedStatement#getId() test.dao.mapper.HouseMapper.select 非空
     * @param result
     * @param row    受影响行数
     * @return
     */
    protected boolean isSqlOnUpdate(String id, Object result, Integer row) {
        if (row == null) {
            return false;
        }
        //如果影响条数=0也打印sql
        if (row == 0) {
            return true;
        }
        if (row <= getMonitorSize()) {
            return false;
        }
        LinkLogUtil.newTagLog("SqlUpdateTooMany")
                .v1(String.valueOf(row))
                .log();
        return true;
    }


    /**
     * 插入场景是否输出日志
     * 可重写
     *
     * @param id     org.apache.ibatis.mapping.MappedStatement#getId() test.dao.mapper.HouseMapper.select 非空
     * @param result
     * @param row    受影响行数
     * @return
     */
    protected boolean isSqlOnInsert(String id, Object result, Integer row) {
        if (row == null) {
            return false;
        }
        //如果影响条数=0也打印sql
        if (row == 0) {
            return true;
        }
        if (row <= getMonitorSize()) {
            return false;
        }
        LinkLogUtil.newTagLog("SqlInsertTooMany")
                .v1(String.valueOf(row))
                .log();
        return true;
    }


    /**
     * 删除场景是否输出日志
     * 可重写
     *
     * @param id     org.apache.ibatis.mapping.MappedStatement#getId() test.dao.mapper.HouseMapper.select 非空
     * @param result
     * @param row    受影响行数
     * @return
     */
    protected boolean isSqlOnDelete(String id, Object result, Integer row) {
        if (row == null) {
            return false;
        }
        //如果影响条数=0也打印sql
        if (row == 0) {
            return true;
        }
        if (row <= getMonitorSize()) {
            return false;
        }
        LinkLogUtil.newTagLog("SqlDeleteTooMany")
                .v1(String.valueOf(row))
                .log();
        return true;
    }


    private int getMonitorSize() {
        String monitorSize = properties.get("monitorSize");
        if (!CommonUtil.isNumeric(monitorSize)) {
            return 200;
        }
        return Integer.parseInt(monitorSize);
    }

    /**
     * org.apache.ibatis.logging.jdbc.BaseJdbcLogger#getParameterValueString()
     *
     * @param boundSql
     * @param parameter
     * @return
     */
    private String getParameterValues(BoundSql boundSql, Object parameter) {
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings == null) {
            return "";
        }
        StringBuilder parameterValues = new StringBuilder();
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                Object value = null;
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) {
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameter != null) {
                    MetaObject metaObject = MetaObject.forObject(parameter, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
                    value = metaObject.getValue(propertyName);
                }

                // 将参数值添加到 StringBuilder 中
                if (i > 0) {
                    parameterValues.append(", ");
                }

                if (value == null) {
                    parameterValues.append("null");
                } else {
                    parameterValues.append(objectValueString(value)).append("(").append(value.getClass().getSimpleName()).append(")");
                }
            }
        }
        return parameterValues.toString();
    }

    /**
     * 去除sql中的换行
     * copy from mybatis
     *
     * @param original
     * @return
     */
    private String removeBreakingWhitespace(String original) {
        StringTokenizer whitespaceStripper = new StringTokenizer(original);
        StringBuilder builder = new StringBuilder();
        while (whitespaceStripper.hasMoreTokens()) {
            builder.append(whitespaceStripper.nextToken());
            builder.append(" ");
        }
        return builder.toString();
    }

    /**
     * 格式化对象
     * 模拟mybatis
     *
     * @param value
     * @return
     */
    protected String objectValueString(Object value) {
        if (value instanceof Array) {
            try {
                return ArrayUtil.toString(((Array) value).getArray());
            } catch (SQLException e) {
                return value.toString();
            }
        } else if (value instanceof Collection || value instanceof Map) {
            return JSON.toJSONString(value);
        } else if (value instanceof Enum || value.getClass().getPackage().getName().startsWith("java.")) {
            return value.toString();
        }
        return JSON.toJSONString(value);
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        if (properties == null || properties.isEmpty()) {
            return;
        }
        for (String key : properties.stringPropertyNames()) {
            if (key == null) {
                continue;
            }
            this.properties.put(key, properties.getProperty(key));
        }
    }
}
